#!/usr/bin/env python2.7
'''
This file is part of Lampy.

Lampy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Lampy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Lampy.  If not, see <http://www.gnu.org/licenses/>.

Copyright 2011 Kevin Han
'''

"""Simple interpreter for a "language" used to play around with lambda calculus."""
import argparse
import re
from string import ascii_letters

import parser
import reducer
import integer

try:
    # provide history and other editing features, on mac and unix platforms
    import readline
except ImportError:
    pass

ALLOWED_NAME_CHARS = ascii_letters + '_-'

INVALID_NAME_ERR = \
        'invalid characters in name "%s" - must only contain [a-zA-Z-_]'

VERSION = '1.1'

class RecursionError(RuntimeError):
    """Provide the node, or None if it was a parser error."""

def printNode(node):
    """
    Print the string representation of a node along with other info according to
    the command line options.
    """
    if args.debug:
        print node

    s = '- ' + parser.unparse(node)

    i = integer.fromNode(node)
    if i is not None:
        s += ' => %d' % i

    print s

def evaluate(s, varDict):
    """
    Return list of nodes showing reduction of a statement and store name=node in
    varDict if s is an assignment.

    Return None if comment or blank line.

    Raise ValueError on syntax errors containing the error message, or
    RecursionError on recursion errors.
    """
    def getIntReplacement(match):
        return '(%s)' % parser.unparse(integer.toNode(int(match.group(0))))

    s = s.strip()
    if '=' in s:
        name, rhs = [val.strip() for val in s.split('=', 1)]
        if not all(c in ALLOWED_NAME_CHARS for c in name):
            raise ValueError(INVALID_NAME_ERR % name)

        try:
            nodes = evaluate(rhs, varDict)
        except RecursionError as err:
            if err[0]:
                # store it anyway if it managed to parse it but not reduce it
                varDict[name] = err[0]
            raise
        else:
            varDict[name] = nodes[-1]
            return nodes
    elif s and not s.startswith('#'):
        for name in varDict:
            s = re.sub(r'\b(%s)\b' % name, r'[\1]', s)
        s = re.sub(r'\b([0-9]+)\b', r'[\1]', s)
        s = s.replace(' ', '')

        if args.debug:
            print 'after direct replacement:', s

        try:
            initial = parser.parse(s, varDict)
        except RuntimeError:
            raise RecursionError(None, )
        except:
            raise ValueError('syntax')

        try:
            return reducer.reduceWithMemo(initial, getSteps=True)
        except RuntimeError:
            raise RecursionError(initial)
    return None

def evalPrintErrs(*args, **kwargs):
    """
    Same as evaluate(), but print error messages and return None on errors.
    """
    try:
        return evaluate(*args, **kwargs)
    except ValueError as err:
        print 'Error:', err
    except RecursionError as err:
        print 'Error: recursion depth exceeded, output may not be fully reduced'
        if err[0]:
            return [err[0]]
    except (KeyboardInterrupt, EOFError):
        print 'Interrupt'

argParser = argparse.ArgumentParser(description='Lampy interpreter.')

argParser.add_argument(
        '-l',
        '--load',
        help='load statements from file(s)',
        metavar='file',
        dest='files',
        default=[],
        nargs='*'
        )
argParser.add_argument(
        '-d',
        '--debug',
        help='print internal python representations',
        action='store_true'
        )
argParser.add_argument(
        '-v',
        '--verbose',
        help='print reduction steps',
        action='store_true'
        )

args = argParser.parse_args()

nameToVal = dict()

##import cProfile
##cProfile.run('evaluate("980", nameToVal)')

print 'Lampy interactive interpreter, v%s' % VERSION

for f in args.files:
    print '* Loading', f
    for line in open(f):
        line = line.strip()
        if line:
            print 'Evaluating "%s"' % line
            evalPrintErrs(line, nameToVal)
    print '* Loaded', f

while True:
    try:
        statement = raw_input('>>> ')
    except (KeyboardInterrupt, EOFError):
        print '\nGoodbye.'
        break

    nodes = evalPrintErrs(statement, nameToVal)

    if nodes is None:
        continue
    if args.verbose:
        for node in nodes:
            printNode(node)
    else:
        printNode(nodes[-1])
